Table of Contents
- Introduction to Functions
- Defining Functions
- Function Parameters
- Return Values
- Variable Scope
- Function Types
- Lambda Functions
- Decorators
- Recursion
- Best Practices
- Common Use Cases
Introduction to Functions
Definition: A function in Python is a reusable block of code that performs a specific task. Functions help break our program into smaller, modular chunks, making it more organized and manageable. They allow code reuse and avoid repetition.
Benefits of using functions
- Code reusability
- Improved readability
- Easier debugging and testing
- Modular programming
- Abstraction of complex operations
Defining Functions
Basic Function Syntax
def function_name(parameters):
"""docstring - optional documentation"""
# function body
# code statements
return value # optional
Simple Function Example
def greet():
"""This function greets the user"""
print("Hello, welcome to Python programming!")
# Calling the function
greet() # Output: Hello, welcome to Python programming!
Function with Parameters
def greet_user(name):
"""Greet a specific user"""
print(f"Hello {name}, welcome to Python programming!")
greet_user("Alice") # Output: Hello Alice, welcome to Python programming!
greet_user("Bob") # Output: Hello Bob, welcome to Python programming!
Function Parameters
Positional Parameters
def describe_pet(animal_type, pet_name):
"""Display information about a pet"""
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet('hamster', 'harry')
# Output:
# I have a hamster.
# My hamster's name is Harry.
Keyword Arguments
def describe_pet(animal_type, pet_name):
"""Display information about a pet"""
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
# Using keyword arguments (order doesn't matter)
describe_pet(pet_name='harry', animal_type='hamster')
describe_pet(animal_type='dog', pet_name='willie')
Default Parameter Values
def describe_pet(pet_name, animal_type='dog'):
"""Display information about a pet with default type"""
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet(pet_name='willie') # Uses default animal_type
describe_pet('harry', 'hamster') # Overrides default
Arbitrary Arguments: *args and **kwargs
# *args for arbitrary positional arguments
def make_pizza(*toppings):
"""Summarize the pizza we are about to make"""
print("\nMaking a pizza with the following toppings:")
for topping in toppings:
print(f"- {topping}")
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
# **kwargs for arbitrary keyword arguments
def build_profile(first, last, **user_info):
"""Build a dictionary containing everything we know about a user"""
user_info['first_name'] = first
user_info['last_name'] = last
return user_info
user_profile = build_profile('albert', 'einstein',
location='princeton',
field='physics')
print(user_profile)
# Output: {'location': 'princeton', 'field': 'physics',
# 'first_name': 'albert', 'last_name': 'einstein'}
Return Values
Returning Simple Values
def get_formatted_name(first_name, last_name):
"""Return a full name, neatly formatted"""
full_name = f"{first_name} {last_name}"
return full_name.title()
musician = get_formatted_name('jimi', 'hendrix')
print(musician) # Output: Jimi Hendrix
Returning Multiple Values
def get_user_info():
"""Return multiple values as a tuple"""
name = "Alice"
age = 30
occupation = "Engineer"
return name, age, occupation # Returns a tuple
user_data = get_user_info()
print(user_data) # Output: ('Alice', 30, 'Engineer')
# Unpacking returned values
name, age, occupation = get_user_info()
print(f"{name} is {age} years old and works as an {occupation}")
Returning Complex Data Structures
def build_car(manufacturer, model, **car_info):
"""Build a dictionary storing information about a car"""
car_info['manufacturer'] = manufacturer
car_info['model'] = model
return car_info
car = build_car('subaru', 'outback', color='blue', tow_package=True)
print(car)
# Output: {'color': 'blue', 'tow_package': True,
# 'manufacturer': 'subaru', 'model': 'outback'}
Variable Scope
Local Variables
def test_local_scope():
local_var = "I'm local to this function"
print(local_var)
test_local_scope() # Output: I'm local to this function
# print(local_var) # This would cause an error - NameError
Global Variables
global_var = "I'm a global variable"
def test_global_scope():
print(global_var) # Can access global variables
test_global_scope() # Output: I'm a global variable
The global Keyword
counter = 0
def increment_counter():
global counter # Declare we're using the global variable
counter += 1
print(f"Before: {counter}") # Output: Before: 0
increment_counter()
increment_counter()
print(f"After: {counter}") # Output: After: 2
Nonlocal Variables (for nested functions)
def outer_function():
outer_var = "I'm in outer function"
def inner_function():
nonlocal outer_var # Refers to variable in the nearest enclosing scope
outer_var = "Modified by inner function"
print(inner_function)
inner_function()
print(outer_var) # Output: Modified by inner function
outer_function()
Function Types
Pure Functions
# Pure function - same input always gives same output, no side effects
def multiply_pure(x, y):
"""Pure function example"""
return x * y
result = multiply_pure(5, 3)
print(result) # Output: 15
Higher-Order Functions
# Functions that take other functions as parameters or return functions
def apply_operation(numbers, operation):
"""Apply operation to each number in the list"""
return [operation(num) for num in numbers]
def square(x):
return x * x
def double(x):
return x * 2
numbers = [1, 2, 3, 4, 5]
squared = apply_operation(numbers, square)
doubled = apply_operation(numbers, double)
print(squared) # Output: [1, 4, 9, 16, 25]
print(doubled) # Output: [2, 4, 6, 8, 10]
Closure Functions
def make_multiplier(factor):
"""Return a function that multiplies by the given factor"""
def multiplier(x):
return x * factor
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # Output: 10
print(triple(5)) # Output: 15
Generator Functions
def countdown(n):
"""Generator function that counts down from n to 0"""
while n >= 0:
yield n
n -= 1
# Using the generator
for number in countdown(5):
print(number) # Output: 5, 4, 3, 2, 1, 0
Lambda Functions
Basic Lambda Syntax
# lambda arguments: expression square = lambda x: x * x print(square(5)) # Output: 25
Using Lambda with Built-in Functions
# Using lambda with map() numbers = [1, 2, 3, 4, 5] squared = list(map(lambda x: x**2, numbers)) print(squared) # Output: [1, 4, 9, 16, 25] # Using lambda with filter() even_numbers = list(filter(lambda x: x % 2 == 0, numbers)) print(even_numbers) # Output: [2, 4] # Using lambda with sorted() points = [(1, 2), (3, 1), (5, 0), (2, 4)] sorted_points = sorted(points, key=lambda point: point[1]) print(sorted_points) # Output: [(5, 0), (3, 1), (1, 2), (2, 4)]
Practical Lambda Examples
# Simple calculator operations
operations = {
'add': lambda x, y: x + y,
'subtract': lambda x, y: x - y,
'multiply': lambda x, y: x * y,
'divide': lambda x, y: x / y if y != 0 else 'Undefined'
}
result = operations['add'](10, 5)
print(result) # Output: 15
result = operations['multiply'](10, 5)
print(result) # Output: 50
Decorators
Basic Decorator
def simple_decorator(func):
"""A simple decorator that adds functionality to a function"""
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@simple_decorator
def say_hello():
print("Hello!")
say_hello()
# Output:
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.
Decorator with Arguments
def repeat(num_times):
"""Decorator that repeats function call specified number of times"""
def decorator_repeat(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"Hello {name}")
greet("Alice")
# Output:
# Hello Alice
# Hello Alice
# Hello Alice
Practical Decorator Example: Timing Functions
import time
def timer(func):
"""Decorator that measures function execution time"""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} executed in {end_time - start_time:.4f} seconds")
return result
return wrapper
@timer
def slow_function():
"""Function that takes some time to execute"""
time.sleep(2)
return "Done"
result = slow_function() # Output: slow_function executed in 2.0023 seconds
Recursion
Basic Recursion Example
def factorial(n):
"""Calculate factorial using recursion"""
if n == 1: # Base case
return 1
else: # Recursive case
return n * factorial(n - 1)
print(factorial(5)) # Output: 120
Fibonacci Sequence with Recursion
def fibonacci(n):
"""Return nth Fibonacci number using recursion"""
if n <= 1:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)
# Print first 10 Fibonacci numbers
for i in range(10):
print(fibonacci(i), end=" ") # Output: 0 1 1 2 3 5 8 13 21 34
Practical Recursion: Directory Traversal
import os
def list_files(startpath):
"""Recursively list all files in a directory"""
for root, dirs, files in os.walk(startpath):
level = root.replace(startpath, '').count(os.sep)
indent = ' ' * 2 * level
print(f"{indent}{os.path.basename(root)}/")
subindent = ' ' * 2 * (level + 1)
for file in files:
print(f"{subindent}{file}")
# Example usage (commented out to avoid actual directory listing):
# list_files('/path/to/directory')
Best Practices
Writing Good Functions
- Use descriptive names: Function names should clearly indicate what they do
- Keep functions small and focused: Each function should do one thing well
- Use docstrings: Document what your function does, its parameters, and return values
- Limit parameters: Functions with too many parameters are hard to use and understand
- Avoid side effects: Functions should generally not modify external state
Example of Well-Designed Function
def calculate_circle_area(radius):
"""
Calculate the area of a circle.
Args:
radius (float): The radius of the circle
Returns:
float: The area of the circle
Raises:
ValueError: If radius is negative
"""
if radius < 0:
raise ValueError("Radius cannot be negative")
return 3.14159 * radius ** 2
try:
area = calculate_circle_area(5)
print(f"Area: {area:.2f}") # Output: Area: 78.54
except ValueError as e:
print(e)
Type Hints (Python 3.5+)
def process_data(data: list[str], reverse: bool = False) -> dict[str, int]:
"""
Process a list of strings and return a dictionary with word counts.
Args:
data: List of strings to process
reverse: Whether to reverse the processing order
Returns:
Dictionary with words as keys and counts as values
"""
result = {}
processed_data = reversed(data) if reverse else data
for item in processed_data:
words = item.split()
for word in words:
result[word] = result.get(word, 0) + 1
return result
sample_data = ["hello world", "world of python", "hello python"]
result = process_data(sample_data)
print(result) # Output: {'hello': 2, 'world': 2, 'of': 1, 'python': 2}
Common Use Cases
Data Processing Functions
def process_csv_data(file_path, delimiter=','):
"""
Read and process CSV data into a list of dictionaries
Args:
file_path: Path to the CSV file
delimiter: Character used to separate values
Returns:
List of dictionaries representing each row
"""
data = []
with open(file_path, 'r') as file:
headers = file.readline().strip().split(delimiter)
for line in file:
values = line.strip().split(delimiter)
row = dict(zip(headers, values))
data.append(row)
return data
# Example usage (would need a actual CSV file):
# data = process_csv_data('data.csv')
# print(data)
Validation Functions
def validate_email(email):
"""
Validate an email address format
Args:
email: Email address to validate
Returns:
bool: True if valid, False otherwise
"""
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
emails = ["test@example.com", "invalid-email", "another.test@domain.co.uk"]
for email in emails:
print(f"{email}: {validate_email(email)}")
# Output:
# test@example.com: True
# invalid-email: False
# another.test@domain.co.uk: True
Utility Functions
def format_currency(amount, currency="USD"):
"""
Format a number as currency
Args:
amount: Numerical amount to format
currency: Currency code (USD, EUR, GBP, etc.)
Returns:
Formatted currency string
"""
currency_symbols = {
"USD": "$",
"EUR": "€",
"GBP": "£",
"JPY": "¥"
}
symbol = currency_symbols.get(currency, "")
return f"{symbol}{amount:,.2f}"
print(format_currency(1234.56)) # Output: $1,234.56
print(format_currency(9876.54, "EUR")) # Output: €9,876.54
print(format_currency(5432.10, "JPY")) # Output: ¥5,432.10
This comprehensive guide covers the essential aspects of user-defined functions in Python, from basic syntax to advanced concepts like decorators and recursion. Mastering functions is crucial for writing clean, efficient, and maintainable Python code.